////////////////////////////////////////////////////////////////////////////////
//
// $Id$ 
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// 
// This software consists of voluntary contributions is licensed under the 
// MIT <http://www.opensource.org/licenses/mit-license.php>. 
// 
// 
// Portuguese Translation
// 
// ESTE SOFTWARE É FORNECIDO “COMO ESTÁ”, SEM GARANTIAS DE QUALQUER ESPÉCIE, 
// EXPRESSAS OU TÁCITAS, INCLUINDO SEM LIMITAÇÕES, QUAISQUER GARANTIA DE COMERCIABILIDADE, 
// DE ADEQUAÇÃO A FINALIDADES ESPECÍFICAS OU DE NÃO VIOLAÇÃO A DIREITOS DE TERCEIROS. 
// EM NENHUMA HIPÓTESE OS AUTORES OU TITULARES DOS DIREITOS AUTORAIS SERÃO RESPONSÁVEIS 
// POR QUAISQUER RECLAMAÇÕES E/OU INDENIZAÇÕES POR DANOS OU POR OUTROS MOTIVOS, 
// SEJAM ESSES DERIVADOS DE OBRIGAÇÕES CONTRATUAIS OU DE DECISÕES JUDICIAIS, 
// ILÍCITOS CIVIS OU OUTROS MOTIVOS, OS QUAIS VENHAM A OCORRER EM CONSEQÜÊNCIA 
// OU EM RELAÇÃO AO SOFTWARE , A SEU USO OU OUTRA FORMA DE NEGOCIAÇÃO/UTILIZAÇÃO. 
// 
// Esse software consiste de contribuições voluntárias e está sobre a licença
// MIT <http://www.opensource.org/licenses/mit-license.php>.
// 
////////////////////////////////////////////////////////////////////////////////

package br.mcaslib.collections
{
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	
	import mx.collections.ArrayCollection;
	import mx.collections.IList;
	import mx.collections.errors.ItemPendingError;
	import mx.core.IPropertyChangeNotifier;
	import mx.events.CollectionEvent;
	import mx.events.CollectionEventKind;
	import mx.events.PropertyChangeEvent;
	import mx.events.PropertyChangeEventKind;
	import mx.resources.ResourceManager;
	import mx.rpc.AbstractOperation;
	import mx.rpc.AsyncToken;
	import mx.rpc.IResponder;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.remoting.RemoteObject;
	import mx.utils.UIDUtil;
	
	[Event(name='loadPage', type='br.mcaslib.collections.ListRemoteViewEvent')]
	
	[Event(name='loadingPage', type='br.mcaslib.collections.ListRemoteViewEvent')]
	
	[Event(name='loadedPage', type='br.mcaslib.collections.ListRemoteViewEvent')]
	
	
	/**
	<p>ListRemoteView is another implementation of IList interface
	dealing with implicit pagination approach, as it, not whole records
	will get from server side just a subset of it.</p> 
	
	<p>The object of this class could be used on Flex control data 
	providers. All pagination manager will be totally transparent
	for Flex control and user (implicit pagination).</p>

	<p>As far as we concerned there is no make sense to implement in
	this class neither filter function nor sort option, because
	this function should be implement in server side, this class
	just views a subset of all records and It's not quite feasible 
	to implement this sort of option in client side like IListCollection.</p>
	
	<p>Take a loot at following simple example to see how to use it</p>
	
	<code>
	<mx:RemoteObject id="remoteObject" destination="amfphp" source="LoadExample" channelSet="{this.channelSet}">
		<mx:method name="load" fault="mx.controls.Alert.show(event.fault.faultString)"/>
	</mx:RemoteObject>
	
	<mcaslib:ListRemoteView id="dpRemote" maxPageLength="100" operation="load" remoteObject="{this.remoteObject}"/>
	
	<mx:DataGrid dataProvider="{this.dpRemote}" width="100%" height="100%">
		<mx:columns>
			<mx:DataGridColumn headerText="Info1" dataField="data1"/>
			<mx:DataGridColumn headerText="Info2" dataField="data2"/>
			<mx:DataGridColumn headerText="Info3" dataField="data3"/>
		</mx:columns>
	</mx:DataGrid>
	</code>
	
	<p>The prototype of LoadExample.load method at server side should be like of</p>
	
	<code>
	LoadExample.load( vo:ListRemoteViewVo, ... args ): Object
	</code>
	
	<p>The args is the same value of arguments property</p>
	
	<p>The vo instance of ListRemoteViewVo type has all properties for use
	to get records from data base or other similar stored</p>
	
	<p>The result Object has to be two property (You can use ListRemoteViewVo
	in result value):</p>
	
	<ul>
	<li><b>realLenght</b> Only if vo.calculateRealLength is set up to true - This is
	a real length of records
	<li><b>data</p> Array of records, the first records os this array is a vo.index'th
	record of the data base store and the max length of this array is vo.maxLength.
	</ul>

	@see ListRemoteViewVo
	*/
	public class ListRemoteView 
		extends EventDispatcher 
		implements IList, IPropertyChangeNotifier, IResponder
	{
		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------

		/**
		 * Constructor
		 */		
		public function ListRemoteView() {
			super();
		}
		
		//--------------------------------------------------------------------------
		//
		//  Variables
		//
		//--------------------------------------------------------------------------

		/**
		 * The list of page. Yes, could be more than one page.
		 * Using more than one page to avoid flicker problem.
		 * @private
		 */
		[ArrayElementType("Page")]
		private var pageList:Array /* of Page */;
		
		/**
		 * Real lenght of the list. 
		 * This is the lenght of the list if was on memory.
		 * The number of -1 means not know real length.
		 * @private
		 */ 
		private var realLength: int = -1;
		
		//--------------------------------------------------------------------------
		//
		//  Overridden properties: <class>
		//
		//--------------------------------------------------------------------------

	    //--------------------------------------------------------------------------
	    //
	    //  Properties: IPropertyChangeNotifier implementation
	    //
	    //--------------------------------------------------------------------------

		//----------------------------------
		//  uid
		//----------------------------------

		/**
		 * Storage for the uid property
		 * @private
		 */  		
		private var _uid:String = UIDUtil.createUID();
	    
	    /**
	     * @copy mx.core.IPropertyChangeNotifier#uid 
	     */  
	    public function get uid():String
	    {
	    	return this._uid;
	    }
	    
	    /**
	     * @private
	     */
	    public function set uid(value:String):void
	    {
	    	this._uid = value;
		}

	    //--------------------------------------------------------------------------
	    //
	    //  Properties: IList implementation
	    //
	    //--------------------------------------------------------------------------

		//----------------------------------
		//  length
		//----------------------------------

		/**
		 * @copy mx.collections.IList#length
		 */ 		
		public function get length():int {
			return this.realLength == -1 ? 0 : this.realLength;
		}
		
	    //--------------------------------------------------------------------------
	    //
	    //  Properties
	    //
	    //--------------------------------------------------------------------------

		//----------------------------------
		//  maxPageLenght
		//----------------------------------
	    
	    /**
	     * Storage for the maxPageLenght property
	     * @private
	     */
	    private var _maxPageLenght:int = 500;
	    
	    /**
	     * The maxPageLenght property means how many register
	     * should be put in one page of ListRemoteView. This is
	     * the max length of one page.
	     * 
	     * You should avoid values less than viewpoint of control.
	     * If the page has less lines of viewpoint's control
	     * it will flicker endless data. 
	     * 
	     * @default 500
	     * @todo Find out away to detect flicker data.
	     */ 
		public function get maxPageLength(): int {
			return this._maxPageLenght;
		} 

		/**
		 * @private
		 */
		public function set maxPageLength(value:int): void {
			this._maxPageLenght = value;
		}
		
		//----------------------------------
		//  maxPage
		//----------------------------------
	    
	    /**
	     * Storage for the maxPage property
	     * @private
	     */
	    private var _maxPage:int = 2;
	    
	    /**
	     * <p>The maxPage property means how many page should be
	     * stored in memory.</p> 
	     * <p><b>Very important:</b>The number of page must be
	     * equal the number of control use this class as dataProvider
	     * to avoid endless flicker looop</p>
	     * <p>The minimum number of page is 2 even if you set 1 page, the system
	     * will change to 2.</p>
	     * 
	     * @default 1
	     */ 
		public function get maxPage(): int {
			return this._maxPage;
		} 

		/**
		 * @private
		 */
		public function set maxPage(value:int): void {
			if( value < 2 ) {
				value = 2;
			}
			this._maxPage = value;
		}
		
		//----------------------------------
		//  missItem
		//----------------------------------

	    /**
	     * Storage for the maxPageLenght property
	     * @private
	     */
	    private var _missItem:Object = null;
	    
	    /**
	     * The missItem is used when get an item out of page,
	     * the missItem will returned instead of PendindErrorException.
	     * 
	     * Sometimes is better to returned a missItem. It gives
	     * a better look.
	     * 
	     * You should set missItem when a control doesn't correctly catch 
	     * PendindErrorExceptionAnother.
	     * 
	     * The type value if missItem should be compatible with all values.  
	     */ 
		public function get missItem():Object {
			return this._missItem;
		} 

		/**
		 * @private
		 */
		public function set missItem(value:Object): void {
			this._missItem = value;
		}
		
		//----------------------------------
		//  operation
		//----------------------------------

	    /**
	     * Storage for the operation property
	     * @private
	     */
	    private var _operation:String;
	    
	    /**
	     * The operation is used to receive data from server.
	     */ 
		public function get operation():String {
			return this._operation;
		} 

		/**
		 * @private
		 */
		public function set operation(value:String):void {
			this._operation = value;
		}

		//----------------------------------
		//  arguments
		//----------------------------------

	    /**
	     * Storage for the arguments property
	     * @private
	     */
	    private var _arguments:Array;
	    
	    /**
	     * The arguments is used to send throught operation to server.
	     */ 
		public function get arguments(): Array {
			return this._arguments;
		} 

		/**
		 * @private
		 */
		public function set arguments(value:Array):void {
			this._arguments = value;
		}
		
		//----------------------------------
		//  remoteObject
		//----------------------------------

	    /**
	     * Storage for the remoteObject property
	     * @private
	     */
	    private var _remoteObject:RemoteObject;
	    
	    /**
	     * The remoteObject is used to receive data from server.
	     */ 
		public function get remoteObject():RemoteObject {
			return this._remoteObject;
		} 

		/**
		 * @private
		 */
		public function set remoteObject(value:RemoteObject):void {
			this._remoteObject = value;
		}

		//--------------------------------------------------------------------------
		//
		//  Overridden methods: <class>
		//
		//--------------------------------------------------------------------------

		//--------------------------------------------------------------------------
		//
		//  Methods: IList implementation
		//
		//--------------------------------------------------------------------------

		/**
		 * @copy mx.collections.IList#addItem
		 */
		public function addItem(item:Object):void {
			var index: int = this.realLength-1;
			if( index < 0 ) {
				index = 0;
			}
			this.addItemAt( item, index );
		}

		/**
		 * @copy mx.collections.IList#addItemAt
		 * 
		 * This will add an item only if this item is into a page.
		 * The implementation of this class represent a view of
		 * remote data therefore don't make sense to change data
		 * that is on server side. When the interface needs this
		 * data it will get from the server.
		 */ 		
		public function addItemAt(item:Object, index:int):void {
			if( this.realLength >= 0 ) {
				var page: Page = this.getNearPage( index );
				page.addItemAt( item, index );
				this.realLength++;
				
				this.startTrackUpdates( item );
				
				this.internalDispatchEvent( CollectionEventKind.ADD, item, index );
			}
		}
		
		/**
		 * @copy mx.collections.IList#getItemAt
		 * 
		 * <p>If the element index isn't on any page, It will
		 * dispatch a remote operation to get another page
		 * os records from server side.</p>
		 * <p>Avoid use this method in for each loop, so all
		 * the itens will get from server side and this class
		 * helps not to do it.</p>  
		 * <p>You should keep up into your mind that this list
		 * is a part view of all itens on server. Perhaps this
		 * all itens on server don't fit in memory</p> 
		 */ 		
		public function getItemAt(index:int, prefetch:int=0):Object {
			var result: Object;
			try {
				var page: Page = this.getPage( index );
				result = page.getItemAt( index );
				
			} catch( e: ItemPendingError ) {
				if( this._missItem ) {
					result = this._missItem;
				} else {
					throw e;
				}
			}
			return result;
		}
		
		/**
		 * @copy mx.collections.IList#getItemIndex
		 * 
		 * If the element index isn't on any page, It will return -1. 
		 */ 		
		public function getItemIndex(item:Object):int {
			var result: int = -1;
			for each( var page: Page in this.pageList ) {
				var index: int = page.getItemIndex( item )
				if( index >=0 ) {
					result = page.indexPageToIndexReal( index )
					break;
				}
			}
			return result;
		}

		/**
		 * @copy mx.collections.IList#itemUpdated
		 * 
		 * If the element index isn't on any page, It will return -1. 
		 */ 		
		public function itemUpdated(item:Object, property:Object=null, oldValue:Object=null, newValue:Object=null):void	{
	        var event:PropertyChangeEvent =
				new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
	        
			event.kind = PropertyChangeEventKind.UPDATE;
	        event.source = item;
	        event.property = property;
	        event.oldValue = oldValue;
	        event.newValue = newValue;
	        
			this.itemUpdateHandler(event);        
		}

		/**
		 * @copy mx.collections.IList#removeAll
		 */ 		
		public function removeAll():void {
			var p: Page = new Page( 0, this.maxPageLength );
			p.source = new Array();
			this.pageList = new Array();
			this.pageList.push( p );
			this.realLength = 0;
			this.internalDispatchEvent( CollectionEventKind.RESET );
		}

		/**
		 * @copy mx.collections.IList#removeItemAt
		 * 
		 * Remove only If the element index is on page.
		 */ 		
		public function removeItemAt(index:int):Object {
			var itemRemoved: Object = null;
			try {
				var page: Page = this.getPage( index, false );
				itemRemoved = page.removeItemAt( index );
				
				this.realLength--;

				this.stopTrackUpdates( itemRemoved );

				this.internalDispatchEvent( CollectionEventKind.REMOVE, itemRemoved, index );				
				
			} catch( e: ItemPendingError ) {
				this.realLength--;
				
			}
			
			return itemRemoved;
		}
		
		/**
		 * @copy mx.collections.IList#setItemAt
		 */ 		
		public function setItemAt(item:Object, index:int):Object {
			var oldItem: Object = null;
			try {
				var page: Page = this.getPage( index, false );
				oldItem = page.setItemAt( item, index );
				this.stopTrackUpdates( oldItem );
				this.startTrackUpdates( item );
				
				var itens:Array = new Array();
				var p:PropertyChangeEvent = new PropertyChangeEvent( PropertyChangeEvent.PROPERTY_CHANGE );
				p.newValue = item;
				p.newValue = oldItem;
				itens.push( p );

				this.internalDispatchEvent( CollectionEventKind.REPLACE, itens, index );				
				
			} catch( e: ItemPendingError ) {
				
			}
			return oldItem;
		}
		
		/**
		 * @copy mx.collections.IList#findItem
		 */ 		
		public function findItem( compare: Function, data: Object ): Object {
			for each ( var page: Page in this.pageList ) {
				for each ( var item: Object in page.source ) {
					if( compare( item, data ) ) {
						return item
					}
				}
			}
			return null;
		}
		
		/**
		 * @copy mx.collections.IList#toArray
		 */ 		
		public function toArray():Array	{
			return this.pageList[0].source;
		}


		//--------------------------------------------------------------------------
		//
		//  Methods: IResponder implementation
		//
		//--------------------------------------------------------------------------

		public function fault(data:Object):void {
			if( data is FaultEvent ) {
				this.faultLoad(FaultEvent(data));
			} else {
				throw new Error( 'It should be a FaultEvent instance object' );
			}
		}
		
		public function result(data:Object):void {
			if( data is ResultEvent ) {
				this.resultLoad(ResultEvent(data));
			} else {
				throw new Error( 'It should be a ResultEvent instance object' );
			}
		}

		//--------------------------------------------------------------------------
		//
		//  Methods
		//
		//--------------------------------------------------------------------------

		/**
		 * Remove all itens from memory and load
		 * the first page (aka refresh).
		 */
		public function refresh(): AsyncToken {
			this.invalidate();
			return this.loadPage(0);
		}
		
		/**
		 * Remove all itens from memory.
		 */
		public function invalidate(): void {
			this.pageList = new Array();
			this.realLength = -1;
		}

	    /** 
	     * Check to see If the item implements an IEventDispatcher, 
	     * so watch it for updates.
	     * 
	     * @param item The item to check   
	     */
	    protected function startTrackUpdates(item:Object):void {
	        if (item && (item is IEventDispatcher))
	        {
	            IEventDispatcher(item).addEventListener(
					                        PropertyChangeEvent.PROPERTY_CHANGE, 
	                                        itemUpdateHandler, false, 0, true);
	        }
	    }
	    
	    /** 
	     * Check to see If the item implements an IEventDispatcher, 
	     * so stop watch it for updates.
	     * 
	     * @param item The item to check  
	     */
	    protected function stopTrackUpdates(item:Object):void {
	        if (item && item is IEventDispatcher)
	        {
	            IEventDispatcher(item).removeEventListener(
					                        PropertyChangeEvent.PROPERTY_CHANGE, 
	                                        itemUpdateHandler);    
	        }
	    }
	    
	    /**
	     * Load the page (a set of records) from the server
	     * 
	     * @param index The index of the first record to load
	     */
		protected function loadPage( index:int ): AsyncToken {
			var asyncToken: AsyncToken
			var loadEvent:ListRemoteViewEvent = new ListRemoteViewEvent( ListRemoteViewEvent.LOAD_PAGE );
			loadEvent.index = index;
			loadEvent.maxLength = this.maxPageLength;

			if( this.realLength == -1 ) {
				loadEvent.calculateRealLength = true;
			}
			
			this.removePendingPage();
			
			this.dispatchEvent( loadEvent );
			
			if( ! loadEvent.isDefaultPrevented() ) {
				
				if( ! this.remoteObject ) {
					throw new Error( 'Property remoteObject was not set up' );
				}
				
				if( ! this.operation ) {
					throw new Error( 'Property operation was not set up' );
				}
				
				var op:AbstractOperation = this.remoteObject.getOperation( this.operation );
				op.cancel();
				
				var args:Array = new Array();
				args.push( ListRemoteViewVo.createFromEvent( loadEvent ) );
				if( this.arguments ) {
					this.arguments.forEach( function(item:*,index:int,array:Array):void { args.push(item) }, this );
				}
				
				asyncToken = op.send.apply( op, args );
				
				if( asyncToken ) {
					var loadingPageEvent: ListRemoteViewEvent = new ListRemoteViewEvent( ListRemoteViewEvent.LOADING_PAGE );
					loadingPageEvent.copyFrom( loadEvent );
					this.dispatchEvent( loadingPageEvent );
					var page: Page = new Page( index, this.maxPageLength );
					this.pageList.push( page );
					asyncToken.page = page;
					asyncToken.addResponder( this );
				}
			}
			
			return asyncToken;
		}	
		
		protected function faultLoad(info:FaultEvent):void {
			this.removePendingPage();
		}
		
		protected function resultLoad(event:ResultEvent):void {
			var eventKind: String = CollectionEventKind.REPLACE;

			// Verify if result is ok. 
			// Has realLength property, Has data property and
			// token has page property.	    	
			if( event.result.hasOwnProperty( 'realLength' ) ) {
				if( this.realLength != event.result.realLength ) {
					this.realLength = event.result.realLength;
					eventKind = CollectionEventKind.RESET;
				}
			} else if( this.realLength < 0 ) {
				throw new Error( "There is no 'realLength' property from remote result" );
			}
			
			if( ! event.result.hasOwnProperty( 'data' ) ) {
				throw new Error( "There is no 'data' property from remote result" );
			}

			if( ! event.token.hasOwnProperty( 'page' ) ) {
				throw new Error( "There is no 'page' property set up within token event result" );
			}
			
			// Tweaks result type (Array or ArrayCollection)
			var resultData: Array
			if( event.result.data ) {
				if( event.result.data is ArrayCollection ) {
					resultData = (event.result.data as ArrayCollection).source;
				} else if( event.result.data is Array ) {
					resultData = event.result.data as Array;
				} else {
					throw new Error( "Neither ArrayCollection nor Array result data type expect" );
				}
			} else {
				resultData = new Array();
			}

			if( resultData.length > this.maxPageLength ) {
				throw new Error( "Remote object returned a array bigger than max page size" );
			}
			
			if( (this.realLength < this.maxPageLength) && (resultData.length < this.realLength) ) {
				throw new Error( "Remote object returned either wrong page size or wrong real length worth" );
			}

			// Set up result data events (propertychange approach)			
			var obj: Object;
			var page: Page = Page(event.token.page);
			for each( obj in resultData ) {
				this.startTrackUpdates( obj );
			}
			page.source = resultData;

			var indexOfCurrentPage:int = this.pageList.indexOf( page );

			// Delete needless pages			
			if( (this.pageList.length > 1) &&
				(this.pageList.length >= this.maxPage ) ) {
					
				// There is more pages than countpage, remove
				var indexPage:int = indexOfCurrentPage;
				while( this.pageList.length > this.maxPage ) {
					indexPage++;
					if( indexPage >= this.pageList.length ) {
						indexPage = 0;
					}
					if( indexPage == indexOfCurrentPage ) {
						break;
					}
					 
					var oldPage: Page = Page(this.pageList.splice( indexPage, 1 )[0]);
					if( ! oldPage.pending ) {
						oldPage.forEach( 
							function (item:*, index:int, array:Array): void {
								stopTrackUpdates( item );
							}
						);
					}
				}
				
			}
			
			this.internalDispatchEvent( eventKind, page.source, page.offset );
						
			var loadedEvent:ListRemoteViewEvent = new ListRemoteViewEvent( ListRemoteViewEvent.LOADED_PAGE );
			this.dispatchEvent( loadedEvent );
		}
		
		/**
		 * Get page which storages index of indexReal's item.
		 * 
		 * @param loadRemote Set true to get item from server.
		 * @param indexReal The index
		 * @private
		 */
		private function getPage( indexReal:int, loadRemote:Boolean = true ): Page {
			// Is out of range?
			if( ((indexReal > this.realLength) && (indexReal < 0)) || (this.realLength < 0) ) {
				throw new RangeError( ResourceManager.getInstance().getString( "collections", "outOfBounds", [indexReal] ) );
			}
			
			// Get page
			var hasPage: Boolean = false;
			var page: Page = null;
			for each ( page in this.pageList ) {
				if( page.indexRealToIndexPage( indexReal ) != -1 ) {
					hasPage = true;
					break;
				}
			}
			
			// Throw an exception for pending item
			if( hasPage ) {
				if( page.pending ) {
					throw new ItemPendingError( "Waiting..." );
				}
			} else {
				if( loadRemote ) {
					var asyncToken: AsyncToken = this.loadPage( indexReal );
					throw new RemoteItemPendingError( asyncToken );
				} else {
					throw new ItemPendingError( "Waiting..." );
				}
			}
			
			return page;
		}

		/**
		 * Get the closest page which storages index of indexReal's item.
		 * 
		 * @param loadRemote Set true to get item from server.
		 * @param indexReal The index
		 * @private
		 */
		private function getNearPage( indexReal:int, loadRemote:Boolean = true ): Page {
			// Is out of range?
			if( ((indexReal > this.realLength - 1) && (indexReal < 0)) || (this.realLength < 0) ) {
				throw new RangeError( ResourceManager.getInstance().getString( "collections", "outOfBounds", [indexReal] ) );
			}
			
			var nearPage: Page = null
			var nearDelta: int = this.realLength+1; // Highest feasible value
			for each ( var page: Page in this.pageList ) {
				var d: int = page.deltaIndexRealToOffset( indexReal );
				if( d < nearDelta ) {
					nearDelta = d;
					nearPage = page;
				}
			}
			
			return nearPage;
		}
		
		/**
		 * Dispatch an event for all listener
		 * @private
		 */
		private function internalDispatchEvent(kind:String, obj:Object = null, location:int = -1):void
		{
			if (hasEventListener(CollectionEvent.COLLECTION_CHANGE)) {
				var event:CollectionEvent =
					new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
				event.kind = kind;
				if( obj != null ) {
					if( obj is Array ) {
						event.items = obj as Array;
					} else {
						event.items.push(obj);
					}
				}
				if( location != -1 ) {
					event.location = location;
				}
				dispatchEvent(event);
			}

	    	if (hasEventListener(PropertyChangeEvent.PROPERTY_CHANGE) && 
	    	   (kind == CollectionEventKind.ADD || kind == CollectionEventKind.REMOVE))	{
	    		var objEvent:PropertyChangeEvent =
					new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
	    		objEvent.property = location;
	    		if (kind == CollectionEventKind.ADD)
	    			objEvent.newValue = obj;
	    		else
	    			objEvent.oldValue = obj;
	    		dispatchEvent(objEvent);
	    	}
	    }
	    
	    /**
	    * Remove all pages waiting to receive records
	    * 
	    * @private
	    */
	    private function removePendingPage():void {
			for ( var i: int = 0; i < this.pageList.length; i++ ) {
				if( Page(this.pageList[i]).pending ) {
					this.pageList.splice( i, 1 );
				}
			}
	    }
	    
		//--------------------------------------------------------------------------
		//
		//  Overridden event handlers
		//
		//--------------------------------------------------------------------------

		//--------------------------------------------------------------------------
		//
		//  Event handlers
		//
		//--------------------------------------------------------------------------

		/**
		 * Listener handler for propertyChange event. This handler is set up
		 * in all itens do listener propertyChange and dispatch this event
		 * to listerners of this collection class.
		 */	    
		protected function itemUpdateHandler( event: PropertyChangeEvent ): void {
			internalDispatchEvent(CollectionEventKind.UPDATE, event);
			if( hasEventListener(PropertyChangeEvent.PROPERTY_CHANGE) ) {
				var objEvent:PropertyChangeEvent = PropertyChangeEvent(event.clone());
				var index:uint = getItemIndex(event.target);
				objEvent.property = index.toString() + "." + event.property;
				dispatchEvent(objEvent);
			}
		}
		
	}
}
	
import mx.collections.errors.ItemPendingError;
import mx.rpc.AsyncToken;
import mx.rpc.IResponder;
import mx.rpc.events.ResultEvent;
import mx.utils.ArrayUtil;

class RemoteItemPendingError extends ItemPendingError {
	
	private var pAsyncToken: AsyncToken;
	
	public function RemoteItemPendingError( asyncToken: AsyncToken ) {
		super( "Waiting..." );
		this.pAsyncToken = asyncToken;
	}
	
	override public function addResponder( responder:IResponder ): void {
		this.pAsyncToken.addResponder( responder );
		super.addResponder( responder );
	}
	
}

class Page {
	
	private var pOffset: int;
	private var pLength: int;
	private var pPending: Boolean;
	private var pSource: Array;
	
	public function Page( offset: int, length: int ) {
		this.pOffset = offset;
		this.pSource = null;
		this.pPending = true;
		this.pLength = length;
	}
	
	public function set source( s: Array ): void {
		this.pPending = false;
		this.pLength = s.length;
		this.pSource = s;
	}
	
	public function get source(): Array {
		return this.pSource;
	}
	
	public function get pending(): Boolean {
		return this.pPending;
	}
	
	public function get offset(): int {
		return this.pOffset;
	}
	
	public function addItemAt( item: Object, indexReal: int ): void {
		this.pLength++;
		var index: int = this.indexRealToIndexPage( indexReal );
		if( index >=0 ) {
			this.pSource.splice( index, 0, item );
		} else {
			this.pLength--;
		}
	}
	
	public function removeItemAt( indexReal: int ): Object {
		var result: Object = null;
		var index: int = this.indexRealToIndexPage( indexReal );
		if( index >=0 ) {
			result = this.pSource.splice( index, 1 )[0];
			this.pLength--;
		}
		return result;
	}

	public function getItemAt( indexReal: int ): Object {
		var result: Object = null;
		var index: int = this.indexRealToIndexPage( indexReal );
		if( index >=0 ) {
			result = this.pSource[index];
		}
		return result;
	}
	
	public function getItemIndex( item: Object ): int {
		return this.pSource.indexOf( item, 0 );
	}
	
	public function setItemAt( item: Object, indexReal: int ): Object {
		var index: int = this.indexRealToIndexPage( indexReal );
		return this.pSource.splice( index, 1, item )[0];
	}
	
	public function forEach( callback:Function ): void {
		this.pSource.forEach( callback );
	} 

	public function indexRealToIndexPage( indexReal: int ): int {
		var result: int = -1;
		if( (indexReal >= this.pOffset) && 
			(indexReal <= this.pOffset + this.pLength - 1) ) {
			result = indexReal - this.pOffset;
		}
		return result;		
	}

	public function deltaIndexRealToOffset( indexReal: int ): int {
		return indexReal - this.pOffset;
	}

	public function indexPageToIndexReal( indexPage: int ): int {
		return this.pOffset + indexPage;
	}
	
}
